Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new package with renderToMarkup export #30105

Merged
merged 8 commits into from
Jun 27, 2024
Merged

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Jun 26, 2024

Name of the package is tbd (straw: react-html). It's a new package separate from react-dom though and can be used as a standalone package - e.g. also from a React Native app.

import {renderToMarkup} from '...';
const html = await renderToMarkup(<Component />);

The idea is that this is a helper for rendering HTML that is not intended to be hydrated. It's primarily intended to support a subset of HTML that can be used as embedding and not served as HTML documents from HTTP. For example as e-mails or in RSS/Atom feeds or other distributions. It's a successor to renderToStaticMarkup.

A few differences:

  • This doesn't support "Client Components". It can only use the Server Components subset. No useEffect, no useState etc. since it will never be hydrated. Use of those are errors.
  • You also can't pass Client References so you can't use components marked with "use client".
  • Unlike renderToStaticMarkup this does support async so you can suspend and use data from these components.
  • Unlike renderToReadableStream this does not support streaming or Suspense boundaries and any error rejects the promise. Since there's no feasible way to "client render" or patch up the document.
  • Form Actions are not supported since in an embedded environment there's no place to post back to across versions. You can render plain forms with fixed URLs though.
  • You can't use any resource preloading like preload() from react-dom.

Implementation

This first version in this PR only supports Server Components since that's the thing that doesn't have an existing API. Might add a Client Components version later that errors.

We don't want to maintain a completely separate implementation for this use case so this uses the dom-legacy build dimension to wire up a build that encapsulates a Flight Server -> Flight Client -> Fizz stream to render Server Components that then get SSR:ed.

There's no problem to use a Flight Client in a Server Component environment since it's already supported for Server-to-Server. Both of these use a bundler config that just errors for Client References though since we don't need any bundling integration and this is just a standalone package.

Running Fizz in a Server Component environment is a problem though because it depends on "react" and it needs the client version. Therefore, for this build we embed the client version of "react" shared internals into the build. It doesn't need anything to be able to use those APIs since you can't call the client APIs anyway.

One unfortunate thing though is that since Flight currently needs to go to binary and back, we need TextEncoder/TextDecoder to be available but this shouldn't really be necessary. Also since we use the legacy stream config, large strings that use byteLengthOfChunk errors atm. This needs to be fixed before shipping. I'm not sure what would be the best layering though that isn't unnecessarily burdensome to maintain. Maybe some kind of pass-through protocol that would also be useful in general - e.g. when Fizz and Flight are in the same process.

Copy link

vercel bot commented Jun 26, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
react-compiler-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 27, 2024 2:02pm

@react-sizebot
Copy link

react-sizebot commented Jun 26, 2024

Comparing: 609d0cc...471c13b

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.68 kB 6.68 kB +0.05% 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 497.93 kB 497.93 kB = 89.26 kB 89.26 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB +0.05% 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 502.75 kB 502.75 kB = 89.96 kB 89.96 kB
facebook-www/ReactDOM-prod.classic.js = 597.10 kB 597.10 kB = 105.31 kB 105.31 kB
facebook-www/ReactDOM-prod.modern.js = 571.44 kB 571.44 kB = 101.24 kB 101.24 kB
oss-experimental/react-html/cjs/react-html.react-server.development.js +∞% 0.00 kB 481.91 kB +∞% 0.00 kB 87.02 kB
oss-experimental/react-html/cjs/react-html.react-server.production.js +∞% 0.00 kB 301.99 kB +∞% 0.00 kB 57.14 kB
oss-experimental/react-html/index.js +∞% 0.00 kB 0.11 kB +∞% 0.00 kB 0.12 kB
oss-experimental/react-html/react-html.react-server.js +∞% 0.00 kB 0.22 kB +∞% 0.00 kB 0.15 kB
oss-stable-rc/react-html/cjs/react-html.react-server.development.js +∞% 0.00 kB 443.71 kB +∞% 0.00 kB 80.75 kB
oss-stable-rc/react-html/cjs/react-html.react-server.production.js +∞% 0.00 kB 282.51 kB +∞% 0.00 kB 54.30 kB
oss-stable-rc/react-html/index.js +∞% 0.00 kB 0.11 kB +∞% 0.00 kB 0.12 kB
oss-stable-rc/react-html/react-html.react-server.js +∞% 0.00 kB 0.22 kB +∞% 0.00 kB 0.15 kB
oss-stable-semver/react-html/cjs/react-html.react-server.development.js +∞% 0.00 kB 443.71 kB +∞% 0.00 kB 80.75 kB
oss-stable-semver/react-html/cjs/react-html.react-server.production.js +∞% 0.00 kB 282.51 kB +∞% 0.00 kB 54.30 kB
oss-stable-semver/react-html/index.js +∞% 0.00 kB 0.11 kB +∞% 0.00 kB 0.12 kB
oss-stable-semver/react-html/react-html.react-server.js +∞% 0.00 kB 0.22 kB +∞% 0.00 kB 0.15 kB
oss-stable/react-html/cjs/react-html.react-server.development.js +∞% 0.00 kB 443.73 kB +∞% 0.00 kB 80.77 kB
oss-stable/react-html/cjs/react-html.react-server.production.js +∞% 0.00 kB 282.53 kB +∞% 0.00 kB 54.33 kB
oss-stable/react-html/index.js +∞% 0.00 kB 0.11 kB +∞% 0.00 kB 0.12 kB
oss-stable/react-html/react-html.react-server.js +∞% 0.00 kB 0.22 kB +∞% 0.00 kB 0.15 kB
test_utils/ReactAllWarnings.js Deleted 62.88 kB 0.00 kB Deleted 15.69 kB 0.00 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react-html/cjs/react-html.react-server.development.js +∞% 0.00 kB 481.91 kB +∞% 0.00 kB 87.02 kB
oss-experimental/react-html/cjs/react-html.react-server.production.js +∞% 0.00 kB 301.99 kB +∞% 0.00 kB 57.14 kB
oss-experimental/react-html/index.js +∞% 0.00 kB 0.11 kB +∞% 0.00 kB 0.12 kB
oss-experimental/react-html/react-html.react-server.js +∞% 0.00 kB 0.22 kB +∞% 0.00 kB 0.15 kB
oss-stable-rc/react-html/cjs/react-html.react-server.development.js +∞% 0.00 kB 443.71 kB +∞% 0.00 kB 80.75 kB
oss-stable-rc/react-html/cjs/react-html.react-server.production.js +∞% 0.00 kB 282.51 kB +∞% 0.00 kB 54.30 kB
oss-stable-rc/react-html/index.js +∞% 0.00 kB 0.11 kB +∞% 0.00 kB 0.12 kB
oss-stable-rc/react-html/react-html.react-server.js +∞% 0.00 kB 0.22 kB +∞% 0.00 kB 0.15 kB
oss-stable-semver/react-html/cjs/react-html.react-server.development.js +∞% 0.00 kB 443.71 kB +∞% 0.00 kB 80.75 kB
oss-stable-semver/react-html/cjs/react-html.react-server.production.js +∞% 0.00 kB 282.51 kB +∞% 0.00 kB 54.30 kB
oss-stable-semver/react-html/index.js +∞% 0.00 kB 0.11 kB +∞% 0.00 kB 0.12 kB
oss-stable-semver/react-html/react-html.react-server.js +∞% 0.00 kB 0.22 kB +∞% 0.00 kB 0.15 kB
oss-stable/react-html/cjs/react-html.react-server.development.js +∞% 0.00 kB 443.73 kB +∞% 0.00 kB 80.77 kB
oss-stable/react-html/cjs/react-html.react-server.production.js +∞% 0.00 kB 282.53 kB +∞% 0.00 kB 54.33 kB
oss-stable/react-html/index.js +∞% 0.00 kB 0.11 kB +∞% 0.00 kB 0.12 kB
oss-stable/react-html/react-html.react-server.js +∞% 0.00 kB 0.22 kB +∞% 0.00 kB 0.15 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-prod.js +0.21% 321.03 kB 321.72 kB +0.16% 56.76 kB 56.85 kB
react-native/implementations/ReactFabric-dev.js = 632.34 kB 625.87 kB = 103.48 kB 102.30 kB
test_utils/ReactAllWarnings.js Deleted 62.88 kB 0.00 kB Deleted 15.69 kB 0.00 kB

Generated by 🚫 dangerJS against 471c13b

We render into an RSC payload using FlightServer, then parse it with
FlightClient and then render the result using Fizz.
{
"name": "react-html",
"version": "19.0.0",
"private": true,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that there's no flag here but the whole package is private so doesn't get publish once we land. We can land and iterate.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was worried this blocks it from being published in react-builds but it works: https://react-builds.vercel.app/api/prs/30105/packages/react-html

packages/react-html/package.json Outdated Show resolved Hide resolved
packages/react-html/README.md Outdated Show resolved Hide resolved
packages/react-html/README.md Outdated Show resolved Hide resolved
"530": "The render was aborted by the server with a promise."
"530": "The render was aborted by the server with a promise.",
"531": "react-html is not supported outside a React Server Components environment.",
"532": "Attempted to render a Client Component from renderToMarkup. This is not supported since it will never hydrate. Only render Server Components with renderToMarkup.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may block people from adopting. E.g. you could render client components with renderToStaticMarkup and were fine with it not being hydrate. renderToMarkup throwing may require a non-trivial refactor. But let's ship and see.

Copy link
Collaborator Author

@sebmarkbage sebmarkbage Jun 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea.

  1. We don't have an option that doesn't involve also allowing them for RSC. Clearly it's a way too easy mistake to import an unmarked Client Component in a Server Component and have it silently no longer work.
  2. The same mistake applies to e-mails too so it's a good thing.

},
"homepage": "https://react.dev/",
"peerDependencies": {
"react": "^19.0.0"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we start off here with the correct approach since renderer and React need to be in lock step?

Suggested change
"react": "^19.0.0"
"react": "19.0.0"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's change all packages at once if we do that.

@eps1lon
Copy link
Collaborator

eps1lon commented Jun 27, 2024

Does this support Edge runtimes like Cloudflare Workers or Edge Light? Seems like it does but would be nice to confirm.

Typo for npm. This isn't covered by tests since our tests manually alias instead of looking at the conditions.

Co-authored-by: Sebastian Silbermann <[email protected]>
sebmarkbage and others added 2 commits June 27, 2024 09:58
Co-authored-by: Sebastian Silbermann <[email protected]>
No longer used since there's no Context on the Server.
@sebmarkbage
Copy link
Collaborator Author

Does this support Edge runtimes like Cloudflare Workers or Edge Light?

Yea. It doesn't use any streams and does synchronous scheduling instead of using macrotasks (which maybe it shouldn't). This first PR depends on TextDecoder/TextEncoder being available on the global object which means it's not actually compatible with older Nodes that doesn't have this. However, as noted, this should be fixed in a follow up.

@sebmarkbage sebmarkbage merged commit ffec9ec into facebook:main Jun 27, 2024
138 checks passed
github-actions bot pushed a commit that referenced this pull request Jun 27, 2024
Name of the package is tbd (straw: `react-html`). It's a new package
separate from `react-dom` though and can be used as a standalone package
- e.g. also from a React Native app.

```js
import {renderToMarkup} from '...';
const html = await renderToMarkup(<Component />);
```

The idea is that this is a helper for rendering HTML that is not
intended to be hydrated. It's primarily intended to support a subset of
HTML that can be used as embedding and not served as HTML documents from
HTTP. For example as e-mails or in RSS/Atom feeds or other
distributions. It's a successor to `renderToStaticMarkup`.

A few differences:

- This doesn't support "Client Components". It can only use the Server
Components subset. No useEffect, no useState etc. since it will never be
hydrated. Use of those are errors.
- You also can't pass Client References so you can't use components
marked with `"use client"`.
- Unlike `renderToStaticMarkup` this does support async so you can
suspend and use data from these components.
- Unlike `renderToReadableStream` this does not support streaming or
Suspense boundaries and any error rejects the promise. Since there's no
feasible way to "client render" or patch up the document.
- Form Actions are not supported since in an embedded environment
there's no place to post back to across versions. You can render plain
forms with fixed URLs though.
- You can't use any resource preloading like `preload()` from
`react-dom`.

## Implementation

This first version in this PR only supports Server Components since
that's the thing that doesn't have an existing API. Might add a Client
Components version later that errors.

We don't want to maintain a completely separate implementation for this
use case so this uses the `dom-legacy` build dimension to wire up a
build that encapsulates a Flight Server -> Flight Client -> Fizz stream
to render Server Components that then get SSR:ed.

There's no problem to use a Flight Client in a Server Component
environment since it's already supported for Server-to-Server. Both of
these use a bundler config that just errors for Client References though
since we don't need any bundling integration and this is just a
standalone package.

Running Fizz in a Server Component environment is a problem though
because it depends on "react" and it needs the client version.
Therefore, for this build we embed the client version of "react" shared
internals into the build. It doesn't need anything to be able to use
those APIs since you can't call the client APIs anyway.

One unfortunate thing though is that since Flight currently needs to go
to binary and back, we need TextEncoder/TextDecoder to be available but
this shouldn't really be necessary. Also since we use the legacy stream
config, large strings that use byteLengthOfChunk errors atm. This needs
to be fixed before shipping. I'm not sure what would be the best
layering though that isn't unnecessarily burdensome to maintain. Maybe
some kind of pass-through protocol that would also be useful in general
- e.g. when Fizz and Flight are in the same process.

---------

Co-authored-by: Sebastian Silbermann <[email protected]>

DiffTrain build for commit ffec9ec.
sebmarkbage added a commit that referenced this pull request Jun 28, 2024
Follow up to #30105.

This supports `renderToMarkup` in a non-RSC environment (not the
`react-server` condition).

This is just a Fizz renderer but it errors at runtime when you use
state, effects or event handlers that would require hydration - like the
RSC version would. (Except RSC can give early errors too.)

To do this I have to move the `react-html` builds to a new `markup`
dimension out of the `dom-legacy` dimension so that we can configure
this differently from `renderToString`/`renderToStaticMarkup`.
Eventually that dimension can go away though if deprecated. That also
helps us avoid dynamic configuration and we can just compile in the
right configuration so the split helps anyway.

One consideration is that if a compiler strips out useEffects or inlines
initial state from useState, then it would not get called an the error
wouldn't happen. Therefore to preserve semantics, a compiler would need
to inject some call that can check the current renderer and whether it
should throw.

There is an argument that it could be useful to not error for these
because it's possible to write components that works with SSR but are
just optionally hydrated. However, there's also an argument that doing
that silently is too easy to lead to mistakes and it's better to error -
especially for the e-mail use case where you can't take it back but you
can replay a queue that had failures. There are other ways to
conditionally branch components intentionally. Besides if you want it to
be silent you can still use renderToString (or better yet
renderToReadableStream).

The primary mechanism is the RSC environment and the client-environment
is really the secondary one that's only there to support legacy
environments. So this also ensures parity with the primary environment.
github-actions bot pushed a commit that referenced this pull request Jun 28, 2024
Follow up to #30105.

This supports `renderToMarkup` in a non-RSC environment (not the
`react-server` condition).

This is just a Fizz renderer but it errors at runtime when you use
state, effects or event handlers that would require hydration - like the
RSC version would. (Except RSC can give early errors too.)

To do this I have to move the `react-html` builds to a new `markup`
dimension out of the `dom-legacy` dimension so that we can configure
this differently from `renderToString`/`renderToStaticMarkup`.
Eventually that dimension can go away though if deprecated. That also
helps us avoid dynamic configuration and we can just compile in the
right configuration so the split helps anyway.

One consideration is that if a compiler strips out useEffects or inlines
initial state from useState, then it would not get called an the error
wouldn't happen. Therefore to preserve semantics, a compiler would need
to inject some call that can check the current renderer and whether it
should throw.

There is an argument that it could be useful to not error for these
because it's possible to write components that works with SSR but are
just optionally hydrated. However, there's also an argument that doing
that silently is too easy to lead to mistakes and it's better to error -
especially for the e-mail use case where you can't take it back but you
can replay a queue that had failures. There are other ways to
conditionally branch components intentionally. Besides if you want it to
be silent you can still use renderToString (or better yet
renderToReadableStream).

The primary mechanism is the RSC environment and the client-environment
is really the secondary one that's only there to support legacy
environments. So this also ensures parity with the primary environment.

DiffTrain build for commit 1e241f9.
@gabrielmfern
Copy link

Stoked for this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants